﻿namespace LuckyAssetManagerContrib.Processors
{
	using System;
	using System.Collections.Generic;
	using System.Globalization;
	using System.Linq;
	using System.Runtime.Caching;
	using System.Text;
	using System.Text.RegularExpressions;
	using System.Web;

	using Lucky.AssetManager.Assets;
	using Lucky.AssetManager.Assets.AssetReaders;
	using Lucky.AssetManager.Processors;

	using Configuration;
	using FileCache;
	using Resources;

	/// <summary>
	/// Asset processor is responsible for final processing assets 
	/// (replacement paths in CSS-files, combining and caching)
	/// </summary>
	public sealed class FinalProcessor : IProcessor
	{
		/// <summary>
		/// Memory cache
		/// </summary>
		private readonly ObjectCache _memCache;

		/// <summary>
		/// File cache manager
		/// </summary>
		private readonly AssetFileCacheManager _fileCacheManager;

		/// <summary>
		/// Configuration settings of asset processor
		/// </summary>
		private readonly FinalProcessorElement _config;

		/// <summary>
		/// Flag that web application is in debug mode
		/// </summary>
		private readonly bool _isDebugMode;

		/// <summary>
		/// Synchronizer of requests to server cache
		/// </summary>
		private static readonly object _cacheSynchronizer = new object();

		/// <summary>
		/// Regular expression for working with paths of components in CSS-code
		/// </summary>
		private static readonly Regex _urlStylesheetRuleRegex =
			new Regex(@"url\((?<quote>'?""?)(?<url>[a-zA-Z0-9а-яА-Я-_\s./?%&:;+=~]+)""?'?\)",
				RegexOptions.IgnoreCase | RegexOptions.Compiled);

		/// <summary>
		/// Regular expression for working with paths of imported stylesheets in CSS-code
		/// </summary>
		private static readonly Regex _importStylesheetRuleRegex =
			new Regex(@"@import\s(?<quote>'?""?)(?<url>[a-zA-Z0-9а-яА-Я-_\s./?%&:;+=~]+)""?'?",
				RegexOptions.IgnoreCase | RegexOptions.Compiled);

		/// <summary>
		/// Gets or sets culture info
		/// </summary>
		public CultureInfo CultureInfo
		{
			get;
			set;
		}


		public FinalProcessor()
			: this(AssetManagerContext.Configuration.CacheFactory.GetCache(), 
				AssetManagerContext.FileCacheManager,
				AssetManagerContext.ContribConfiguration.FinalProcessor, 
				HttpContext.Current.IsDebuggingEnabled)
		{ }

		public FinalProcessor(ObjectCache memCache, AssetFileCacheManager fileCacheManager, 
			FinalProcessorElement config, bool isDebugMode)
		{
			_memCache = memCache;
			_fileCacheManager = fileCacheManager;
			_config = config;
			_isDebugMode = isDebugMode;
		}


		/// <summary>
		/// Performs final processing of assets
		/// </summary>
		/// <param name="assets">Set of assets</param>
		/// <returns>Set of processed assets</returns>
		public IEnumerable<IAsset> Process(IEnumerable<IAsset> assets)
		{
			if (assets == null)
			{
				throw new ArgumentException(Strings.Common_ValueIsNull, "assets");
			}

			List<IAsset> assetList = assets.ToList();
			if (assetList.Count <= 1)
			{
				// If number of assets does not exceed one, then 
				// stop processing and return  assets as is
				return assetList;
			}

			List<IAsset> results = assetList
				.Where(a => !a.IsProcessable)
				.ToList()
				;

			List<IAsset> assetsToProcessing = assetList
				.Where(a => a.IsProcessable)
				.ToList()
				;

			if (_config.DisableCombiningAssetsInDebugMode
				&& _isDebugMode)
			{
				// If settings indicate that you want to disable combining 
				// of assets in debug mode and web application is in this 
				// mode, then not make combining of assets
				foreach (IAsset asset in assetsToProcessing)
				{
					if (asset.IsInMemory)
					{
						results.Add(ProcessInMemoryAsset(asset));
					}
					else
					{
						results.Add(asset);
					}
				}
			}
			else
			{
				// Otherwise, make combining of assets
				List<IGrouping<IAssetKey, IAsset>> assetGroups = assetsToProcessing
					.GroupBy(a => a.Key)
					.ToList()
					;

				foreach (IGrouping<IAssetKey, IAsset> assetGroup in assetGroups)
				{
					if (assetGroup.Count() == 1)
					{
						IAsset asset = assetGroup.Single();
						if (asset.IsInMemory)
						{
							results.Add(ProcessInMemoryAsset(asset));
						}
						else
						{
							results.Add(asset);
						}
					}
					else
					{
						var assetsInGroup = assetGroup
							.OrderByDescending(a => a.OnLayoutPage)
							.ToList()
							;

						results.Add(ProcessAssetGroup(assetsInGroup));
					}
				}
			}

			return results;
		}

		/// <summary>
		/// Processes assets whose content is read into memory
		/// </summary>
		/// <param name="asset">In-memory asset</param>
		/// <returns>Processed asset</returns>
		private IAsset ProcessInMemoryAsset(IAsset asset)
		{
			lock (_cacheSynchronizer)
			{
				IAssetReader assetReader = asset.Reader;
				IList<string> associatedFilePaths = assetReader.AssociatedFilePaths;
				string assetPath = asset.Path;
				DateTime lastModifyTime;

				// Trying to read data from cache
				string cachedContent = GetAssetContentFromCache(asset, assetPath,
					assetReader, out lastModifyTime);

				string newContent = cachedContent;
				if (newContent == null)
				{
					newContent = asset.Reader.Content;
					if (Utils.IsCssAsset(asset))
					{
						// Transform relative paths to absolute in CSS-code
						newContent = ResolveImportsRelativePaths(newContent, asset.Path);
						newContent = ResolveComponentsRelativePaths(newContent, asset.Path);
					}

					// Caches text content of processed asset
					AddAssetContentToCache(asset, assetPath, assetReader, 
						newContent, out lastModifyTime);
				}

				asset.Path = AddVersionNumberToAssetFilePath(assetPath, lastModifyTime);
				asset.Reader = new MemoryAssetReader(associatedFilePaths, newContent);

				return asset;
			}
		}

		/// <summary>
		/// Combines group of assets to single asset
		/// </summary>
		/// <param name="assetsInGroup">Group of assets</param>
		/// <returns>Combined asset</returns>
		private IAsset ProcessAssetGroup(IList<IAsset> assetsInGroup)
		{
			lock (_cacheSynchronizer)
			{
				var associatedFilePaths = new List<string>();
				DateTime lastModifyTime;

				foreach (IAsset asset in assetsInGroup)
				{
					associatedFilePaths.AddRange(asset.Reader.AssociatedFilePaths);
				}

				// Gets sample asset
				IAsset sampleAsset = assetsInGroup.First();

				// Calculates path for combined asset
				string assetPath = sampleAsset.Path;
				string newPath = assetPath;

				if (Utils.IsCssAsset(assetPath))
				{
					newPath = Constants.FakeCombinedCssAssetFilePath;
				}
				else if (Utils.IsJavaScriptAsset(assetPath))
				{
					newPath = Constants.FakeCombinedJavaScriptAssetFilePath;
				}

				// Creates new asset reader
				IAssetReader newAssetReader = new MemoryAssetReader(associatedFilePaths, String.Empty);

				// Trying to read data from cache
				string cachedContent = GetAssetContentFromCache(sampleAsset, newPath,
					newAssetReader, out lastModifyTime);

				string newContent = cachedContent;
				if (newContent == null)
				{
					// If cache item is empty, then read text contents 
					// from files of asset group and then combine it
					var combinedTextBuilder = new StringBuilder();

					foreach (IAsset asset in assetsInGroup)
					{
						string content = asset.Reader.Content;
						if (Utils.IsCssAsset(asset))
						{
							// Transform relative paths to absolute in CSS-code
							content = ResolveImportsRelativePaths(content, asset.Path);
							content = ResolveComponentsRelativePaths(content, asset.Path);
						}
						combinedTextBuilder.AppendLine(content);
					}

					newContent = combinedTextBuilder.ToString();

					// Caches text content of combined asset
					AddAssetContentToCache(sampleAsset, newPath, newAssetReader, 
						newContent, out lastModifyTime);
				}

				// Creates new asset that will store code of combined assets
				IAsset newAsset = assetsInGroup.First();
				newAsset.Path = AddVersionNumberToAssetFilePath(newPath, lastModifyTime);
				newAsset.Reader = new MemoryAssetReader(associatedFilePaths, newContent);

				return newAsset;
			}
		}

		/// <summary>
		/// Adds version number to asset file path
		/// </summary>
		/// <param name="assetPath">Asset file path</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		/// <returns>New asset file path</returns>
		private string AddVersionNumberToAssetFilePath(string assetPath, DateTime lastModifyTime)
		{
			string newAssetPath = assetPath.ToLower();
			if (lastModifyTime != DateTime.MinValue)
			{
				newAssetPath += String.Format("?ver={0}", 
					Utils.ConvertDateTimeToHashString(lastModifyTime));
			}

			return newAssetPath;
		}

		/// <summary>
		/// Calculates base key of cache item, which 
		/// contains text content of processed asset
		/// </summary>
		/// <param name="sampleAsset">Sample asset</param>
		/// <param name="assetPath">Asset file path</param>
		/// <param name="assetReader">Asset reader</param>
		/// <returns>Cache item base key</returns>
		private string GenerateCacheItemBaseKey(IAsset sampleAsset, string assetPath, 
			IAssetReader assetReader)
		{
			string baseKey = GenerateCacheItemKey(sampleAsset, assetPath, 
				assetReader, DateTime.MinValue);

			return baseKey;
		}

		/// <summary>
		/// Calculates key of cache item, which 
		/// contains text content of processed asset
		/// </summary>
		/// <param name="sampleAsset">Sample asset</param>
		/// <param name="assetPath">Asset file path</param>
		/// <param name="assetReader">Asset reader</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		/// <returns>Cache item key</returns>
		private string GenerateCacheItemKey(IAsset sampleAsset, string assetPath,
			IAssetReader assetReader, DateTime lastModifyTime)
		{
			// Add version number to asset file path
			string newPath = AddVersionNumberToAssetFilePath(assetPath, lastModifyTime);

			// Remember old values of asset path and asset reader
			string oldPath = sampleAsset.Path;
			IAssetReader oldAssetReader = sampleAsset.Reader;

			// Assign new value of asset file path to compute new hash code
			sampleAsset.Path = newPath;
			sampleAsset.Reader = assetReader;

			// Compute cache item key
			string key = sampleAsset.GetHashCode().ToString(CultureInfo.InvariantCulture);

			// Restore old values of asset file path and asset reader
			sampleAsset.Path = oldPath;
			sampleAsset.Reader = oldAssetReader;

			return key;
		}

		/// <summary>
		/// Gets cache item key by base key
		/// </summary>
		/// <param name="cacheItemBaseKey">Cache item base key</param>
		/// <returns>Cache item key</returns>
		private string GetCacheItemKey(string cacheItemBaseKey)
		{
			string cacheItemKey = String.Empty;
	
			string lastCacheItemKey = String.Format(Constants.LastCacheItemKeyPattern, cacheItemBaseKey);
			object cacheItem = _memCache.Get(lastCacheItemKey);
			if (cacheItem != null)
			{
				cacheItemKey = (string)cacheItem;
			}

			return cacheItemKey;
		}

		/// <summary>
		/// Gets text content of processed asset from cache
		/// </summary>
		/// <param name="sampleAsset">Sample asset</param>
		/// <param name="assetPath">Asset file path</param>
		/// <param name="assetReader">Asset reader</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		/// <returns>Text content of processed asset</returns>
		private string GetAssetContentFromCache(IAsset sampleAsset, string assetPath,
			IAssetReader assetReader, out DateTime lastModifyTime)
		{
			string cacheItemBaseKey = GenerateCacheItemBaseKey(sampleAsset, assetPath, 
				assetReader);
			string cacheItemKey = GetCacheItemKey(cacheItemBaseKey);
			lastModifyTime = DateTime.MinValue;

			object contentCacheItem = null;
			if (!String.IsNullOrWhiteSpace(cacheItemKey))
			{
				_fileCacheManager.UpdateItemLastAccessTime(cacheItemKey);

				// Get text content of processed asset from cache
				contentCacheItem = _memCache.Get(cacheItemKey);

				// Get date and time of last modification files 
				// associated with processed asset from cache
				string lastModifyTimeCacheItemKey = String.Format(
					Constants.ProcessedAssetLastModifyTimeCacheItemKeyPattern,
					cacheItemKey);
				object lastModifyTimeCacheItem = _memCache.Get(lastModifyTimeCacheItemKey);
				if (lastModifyTimeCacheItem != null)
				{
					lastModifyTime = (DateTime)lastModifyTimeCacheItem;
				}
			}

			return (string)contentCacheItem;
		}

		/// <summary>
		/// Adds text content of processed asset to cache
		/// </summary>
		/// <param name="sampleAsset">Sample asset</param>
		/// <param name="assetPath">Asset file path</param>
		/// <param name="assetReader">Asset reader</param>
		/// <param name="content">Text content of processed asset</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		private void AddAssetContentToCache(IAsset sampleAsset, string assetPath,
			IAssetReader assetReader, string content, out DateTime lastModifyTime)
		{
			IList<string> associatedFilePaths = assetReader.AssociatedFilePaths;

			// Get date and time of last modification files associated 
			// with processed asset
			lastModifyTime = Utils.GetLastModifyTimeOfFiles(associatedFilePaths.ToArray());

			string cacheItemBaseKey = GenerateCacheItemBaseKey(sampleAsset, assetPath, 
				assetReader);
			string cacheItemKey = GenerateCacheItemKey(sampleAsset, assetPath, 
				assetReader, lastModifyTime);

			// Make early caching text content of processed asset in memory cache
			var contentCacheMonitor = new HostFileChangeMonitor(associatedFilePaths);

			var contentCachePolicy = new CacheItemPolicy();
			contentCachePolicy.ChangeMonitors.Add(contentCacheMonitor);

			_memCache.Add(new CacheItem(cacheItemKey, content), contentCachePolicy);

			// Cache date and time of last modification files associated 
			// with processed asset
			string lastModifyTimeCacheItemKey = String.Format(
				Constants.ProcessedAssetLastModifyTimeCacheItemKeyPattern,
				cacheItemKey);

			var lastModifyTimeCacheMonitor = _memCache.CreateCacheEntryChangeMonitor(new[] { cacheItemKey });

			var lastModifyTimeCachePolicy = new CacheItemPolicy
			{
				Priority = CacheItemPriority.NotRemovable
			};
			lastModifyTimeCachePolicy.ChangeMonitors.Add(lastModifyTimeCacheMonitor);

			_memCache.Add(new CacheItem(lastModifyTimeCacheItemKey, lastModifyTime), 
				lastModifyTimeCachePolicy);

			// Make caching of cache item key containing latest version 
			// text content of  processed asset
			string lastCacheItemKey = String.Format(
				Constants.LastCacheItemKeyPattern,
				cacheItemBaseKey);

			var keyCacheMonitor = _memCache.CreateCacheEntryChangeMonitor(new[] { cacheItemKey });

			var keyCachePolicy = new CacheItemPolicy
			{
				Priority = CacheItemPriority.NotRemovable
			};
			keyCachePolicy.ChangeMonitors.Add(keyCacheMonitor);

			_memCache.Add(new CacheItem(lastCacheItemKey, cacheItemKey), 
				keyCachePolicy);

			// Cache processed asset in file cache (need to ensure proper 
			// of caching ASP.NET-page output)
			_fileCacheManager.InsertItem(cacheItemKey, content, associatedFilePaths.ToArray(), 
				lastModifyTime);
		}

		/// <summary>
		/// Transforms relative paths of components to absolute in CSS-code
		/// </summary>
		/// <param name="content">Text content of CSS-asset</param>
		/// <param name="path">CSS-file path</param>
		/// <returns>Processed text content of CSS-asset</returns>
		private static string ResolveComponentsRelativePaths(string content, string path)
		{
			return _urlStylesheetRuleRegex.Replace(content, m =>
			{
				string result = m.Groups[0].Value;

				if (m.Groups["url"].Success)
				{
					string urlValue = m.Groups["url"].Value;
					string quoteValue = m.Groups["quote"].Success ? m.Groups["quote"].Value : "";

					result = String.Format("url({0}{1}{2})",
						quoteValue,
						Utils.TransformRelativeUrlToAbsolute(path, urlValue),
						quoteValue);
				}

				return result;
			});
		}

		/// <summary>
		/// Transforms relative paths of imported stylesheets to absolute in CSS-code
		/// </summary>
		/// <param name="content">Text content of CSS-asset</param>
		/// <param name="path">CSS-file path</param>
		/// <returns>Processed text content of CSS-asset</returns>
		private static string ResolveImportsRelativePaths(string content, string path)
		{
			return _importStylesheetRuleRegex.Replace(content, m =>
			{
				string result = m.Groups[0].Value;

				if (m.Groups["url"].Success)
				{
					string urlValue = m.Groups["url"].Value;
					string quoteValue = m.Groups["quote"].Success ? m.Groups["quote"].Value : "";

					result = String.Format("@import {0}{1}{2}",
						quoteValue,
						Utils.TransformRelativeUrlToAbsolute(path, urlValue),
						quoteValue);
				}

				return result;
			});
		}
	}
}